/**
 * 代码混淆脚本，兼容nodejs@12.x.x到20.x.x
 * @author 耿凯博
 * 1. 设置需要混淆的文件，入参为文件目录或对应文件的路径如：'./src/Common/Commonjs'  或 './src/Common/Commonjs/index.js'
 * 2. 设置需要被禁用混淆的字符，因为如果不设置，require('dayjs')将变成require(_0xxxx[_0xxxx])导致代码无法正常运行
 *
 * 命令行：
 * npm run code:gen 执行混淆命令（默认混淆文件将替换掉原始文件，并新增后缀为‘_backup’的原始文件备份，可根据config进行配置）
 * npm run code:recover 执行恢复命令（恢复原始文件，删除备份文件）
 *
 * 注意事项：
 * 在项目使用该脚本时，记得在gitignore中忽略以‘_backup’为后缀的js文件，避免上传原始文件代码
 */
const fs = require('fs')
const path = require('path')
const JavaScriptObfuscator = require('javascript-obfuscator')

// 1. 设置需要混淆的文件
const source = ['./src']

// 2. 设置需要被禁用混淆的字符
const reservedStrings = ['dayjs']

let config = {
  // 是否恢复混淆
  recover: false,
  // 生成的混淆文件: 名称末尾追加字符
  // obfuscatedFilenameAppend: '_obfuscated',
  // 生成的混淆文件: 直接替换原始文件
  obfuscatedFilenameAppend: '',
  // 备份原始文件: 名称末尾追加字符
  backupFilenameAppend: '_backup',
}

// 解析命令行参数
process.argv.slice(2).forEach((arg) => {
  const [_key, value] = arg.split('=')
  const key = _key.replace('--', '')
  config[key] = value || true
})

for (let i = 0; i < source.length; i++) {
  const filePath = source[i]
  // 提取目录部分（前缀）
  const directoryPrefix = path.dirname(filePath)
  // 提取文件名如：index.js
  const fileNameWithExtension = filePath.split('/').pop()
  // 提取文件名称如：index
  const fileName = fileNameWithExtension
    .split('.')
    .slice(0, -1)
    .join('.')
  // 提取文件名后缀如：.js
  const extension = fileNameWithExtension.split('.').pop()
  // 备份地址
  const backupOutputFilePath = `${directoryPrefix}/${fileName}${config.backupFilenameAppend}.${extension}`
  // 混淆地址
  const obfuscatorOutputFilePath = `${directoryPrefix}/${fileName}${config.obfuscatedFilenameAppend}.${extension}`

  if (fileName && extension) {
    // 单文件处理
    if (extension !== 'js' || filePath.includes(config.backupFilenameAppend)) {
      continue
    }
    obf({
      recover: config.recover,
      filePath: filePath,
      backupOutputFilePath: backupOutputFilePath,
      obfuscatorOutputFilePath: obfuscatorOutputFilePath,
    })
  } else {
    // 目录处理
    getAllFiles(filePath).then((files) => {
      for (let i = 0; i < files.length; i++) {
        const filePath2 = filePath + '/' + files[i]
        const directoryPrefix2 = path.dirname(filePath2)
        // 提取文件名如：index.js
        const fileNameWithExtension2 = filePath2.split('/').pop()
        // 提取文件名称如：index
        const fileName2 = fileNameWithExtension2
          .split('.')
          .slice(0, -1)
          .join('.')
        // 提取文件名后缀如：.js
        const extension2 = fileNameWithExtension2.split('.').pop()
        // 备份地址
        const backupOutputFilePath2 = `${directoryPrefix2}/${fileName2}${config.backupFilenameAppend}.${extension2}`
        // 混淆地址
        const obfuscatorOutputFilePath2 = `${directoryPrefix2}/${fileName2}${config.obfuscatedFilenameAppend}.${extension2}`
        if (extension2 !== 'js' || filePath2.includes(config.backupFilenameAppend)) {
          continue
        }
        obf({
          recover: config.recover,
          filePath: filePath2,
          backupOutputFilePath: backupOutputFilePath2,
          obfuscatorOutputFilePath: obfuscatorOutputFilePath2,
        })
      }
    })
  }
}

function obf(
  options = {
    recover: false,
    filePath: '',
    backupOutputFilePath: '',
    obfuscatorOutputFilePath: '',
  }
) {
  if (!options.recover) {
    // 读取文件内容
    fs.readFile(options.filePath, 'utf8', (err, data) => {
      if (!data) return
      if (err) {
        console.log(`Error reading file ${options.filePath}: ${err}`)
        return
      }
      // 如果已经混淆，则不做处理
      if (fs.existsSync(options.backupOutputFilePath) && data.indexOf('_0x') > -1) {
        console.log(options.filePath + `已经混淆完成，请勿重复调用`)
        return
      }
      // 使用javascript-obfuscator混淆代码
      const obfuscationResult = JavaScriptObfuscator.obfuscate(data, {
        compact: false,
        stringArray: true,
        reservedStrings: reservedStrings,
      })
      const obfuscatedCode = obfuscationResult.getObfuscatedCode()

      // 备份
      fs.writeFile(options.backupOutputFilePath, data, (err) => {
        if (err) {
          console.log(`备份失败： ${options.backupOutputFilePath}: ${err}`)
          return
        }
        console.log(`备份代码已保存至： ${options.backupOutputFilePath}`)
      })

      // 混淆
      fs.writeFile(options.obfuscatorOutputFilePath, obfuscatedCode, (err) => {
        if (err) {
          console.log(`混淆失败： ${options.obfuscatorOutputFilePath}: ${err}`)
          return
        }
        console.log(`混淆代码已保存至： ${options.obfuscatorOutputFilePath}`)
      })
    })
  } else {
    fs.readFile(options.backupOutputFilePath, 'utf8', (err, data) => {
      if (!data) return
      fs.writeFile(options.filePath, data, (err) => {
        if (err) {
          console.log(`恢复失败： ${options.filePath}: ${err}`)
          return
        }
        console.log(`恢复代码已保存至： ${options.filePath}`)
        // 移除备份文件
        fs.unlinkSync(options.backupOutputFilePath)
        // 移除混淆文件
        if (options.obfuscatorOutputFilePath != options.filePath) {
          fs.unlinkSync(options.obfuscatorOutputFilePath)
        }
      })
    })
  }
}

// 获取文件夹下的所有文件相对路径的集合
async function getAllFiles(dir) {
  const files = []

  async function traverse(directory, parentCatlog) {
    const dirents = await fs.promises.opendir(directory, { withFileTypes: true })

    for await (const dirent of dirents) {
      const fullPath = path.join(directory, dirent.name)
      if (dirent.isDirectory()) {
        // 如果是目录，则递归遍历
        await traverse(fullPath, (parentCatlog ? parentCatlog + '/' : '') + dirent.name)
      } else {
        // 如果是文件，则添加到文件列表中
        files.push((parentCatlog ? parentCatlog + '/' : '') + dirent.name)
      }
    }
  }

  await traverse(dir)
  return files
}
